package com.mikhaellopez.circularprogressbar;

import ohos.agp.animation.AnimatorValue;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrHelper;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.render.Arc;
import ohos.agp.render.Canvas;
import ohos.agp.render.LinearShader;
import ohos.agp.render.Paint;
import ohos.agp.render.Shader;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.global.resource.NotExistException;
import ohos.global.resource.WrongTypeException;

import java.io.IOException;

/**
 * Copyright (C) 2019 Mikhael LOPEZ
 * Licensed under the Apache License Version 2.0
 */
public class CircularProgressBar extends Component implements Component.EstimateSizeListener,
    Component.DrawTask, Component.LayoutRefreshedListener, Component.BindStateChangedListener {
    private static final float DEFAULT_MAX_VALUE = 100.0F;
    private static final float DEFAULT_START_ANGLE = 270.0F;
    private static final long DEFAULT_ANIMATION_DURATION = 1550L;

    // Properties
    private AnimatorValue progressAnimator;
    private EventHandler indeterminateModeHandler;

    // View
    private Rect rectF;
    private Paint backgroundPaint;
    private Paint foregroundPaint;

    // region Attributes
    private float progress;
    private float progressMax;
    private float progressBarWidth;
    private float backgroundProgressBarWidth;
    private Color progressBarColor;
    private Color progressBarColorStart;
    private Color progressBarColorEnd;
    private GradientDirection progressBarColorDirection;
    private Color backgroundProgressBarColor;
    private Color backgroundProgressBarColorStart;
    private Color backgroundProgressBarColorEnd;
    private GradientDirection backgroundProgressBarColorDirection;
    private boolean roundBorder;
    private float startAngle;
    private ProgressDirection progressDirection;
    private boolean indeterminateMode;
    private ProgressChangeListener onProgressChangeListener;
    private IndeterminateModeChangeListener onIndeterminateModeChangeListener;

    // endregion

    // region Indeterminate Mode
    private float progressIndeterminateMode;
    private ProgressDirection progressDirectionIndeterminateMode;
    private float startAngleIndeterminateMode = DEFAULT_START_ANGLE;

    private Runnable indeterminateModeRunnable;

    public CircularProgressBar(Context context, AttrSet attrSet) {
        this(context, attrSet, "");
    }

    public CircularProgressBar(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        rectF = new Rect();
        backgroundPaint = new Paint();
        backgroundPaint.setAntiAlias(true);
        backgroundPaint.setStyle(Paint.Style.STROKE_STYLE);
        foregroundPaint = new Paint();
        foregroundPaint.setAntiAlias(true);
        foregroundPaint.setStyle(Paint.Style.STROKE_STYLE);
        progress = 0f;
        progressMax = DEFAULT_MAX_VALUE;
        try {
            progressBarWidth = getResourceManager().getElement(ResourceTable.Float_default_stroke_width).getFloat();
            backgroundProgressBarWidth = getResourceManager()
                .getElement(ResourceTable.Float_default_background_stroke_width).getFloat();
        } catch (IOException | WrongTypeException | NotExistException e) {
            e.printStackTrace();
        }
        progressBarColor = Color.BLACK;
        progressBarColorDirection = GradientDirection.LEFT_TO_RIGHT;
        backgroundProgressBarColor = Color.GRAY;
        backgroundProgressBarColorDirection = GradientDirection.LEFT_TO_RIGHT;
        roundBorder = false;
        startAngle = DEFAULT_START_ANGLE;
        progressDirection = ProgressDirection.TO_RIGHT;
        indeterminateMode = false;
        progressDirectionIndeterminateMode = ProgressDirection.TO_RIGHT;
        startAngleIndeterminateMode = DEFAULT_START_ANGLE;
        indeterminateModeRunnable = () -> {
            if (indeterminateMode) {
                postIndeterminateModeHandler();
                progressDirectionIndeterminateMode = reverse(progressDirectionIndeterminateMode);
                if (isToRight(progressDirectionIndeterminateMode)) {
                    setProgressWithAnimation(0f, 1500L, null, null);
                } else {
                    setProgressWithAnimation(progressMax, 1500L, null, null);
                }
            }
        };

        init(attrSet);
    }

    private void init(AttrSet attrSet) {
        // Value
        setProgress(AttrValue.get(attrSet, "cpb_progress", progress));
        setProgressMax(AttrValue.get(attrSet, "cpb_progress_max", progressMax));

        // StrokeWidth
        setProgressBarWidth(pxToVp(AttrValue.getDimension(attrSet, "cpb_progressbar_width", (int) progressBarWidth)));
        setBackgroundProgressBarWidth(pxToVp(AttrValue.getDimension(attrSet,
            "cpb_background_progressbar_width", (int) backgroundProgressBarWidth)));

        // Color
        setProgressBarColor(AttrValue.get(attrSet, "cpb_progressbar_color", progressBarColor));
        Color progressbarColorStart = AttrValue.get(attrSet, "cpb_progressbar_color_start", new Color());
        if (progressbarColorStart.getValue() != 0) {
            setProgressBarColorStart(progressbarColorStart);
        }
        Color progressbarColorEnd = AttrValue.get(attrSet, "cpb_progressbar_color_end", new Color());
        if (progressbarColorEnd.getValue() != 0) {
            setProgressBarColorEnd(progressbarColorEnd);
        }
        setProgressBarColorDirection(toGradientDirection(AttrValue.get(attrSet,
            "cpb_progressbar_color_direction", progressBarColorDirection.value)));
        setBackgroundProgressBarColor(AttrValue.get(attrSet,
            "cpb_background_progressbar_color", backgroundProgressBarColor));
        Color bgColorStart = AttrValue.get(attrSet,
            "cpb_background_progressbar_color_start", new Color());
        if (bgColorStart.getValue() != 0) {
            setBackgroundProgressBarColorStart(bgColorStart);
        }
        Color bgColorEnd = AttrValue.get(attrSet,
            "cpb_background_progressbar_color_end", new Color());
        if (bgColorEnd.getValue() != 0) {
            setBackgroundProgressBarColorEnd(bgColorEnd);
        }
        setBackgroundProgressBarColorDirection(toGradientDirection(AttrValue.get(attrSet,
            "cpb_background_progressbar_color_direction", backgroundProgressBarColorDirection.value)));

        // Progress Direction
        setProgressDirection(toProgressDirection(AttrValue.get(attrSet,
            "cpb_progress_direction", progressDirection.value)));

        // Round Border
        setRoundBorder(AttrValue.get(attrSet, "cpb_round_border", roundBorder));

        // Angle
        setStartAngle(AttrValue.get(attrSet, "cpb_start_angle", 0f));

        // Indeterminate Mode
        setIndeterminateMode(AttrValue.get(attrSet, "cpb_indeterminate_mode", indeterminateMode));

        setEstimateSizeListener(this);
        addDrawTask(this);
        setLayoutRefreshedListener(this);
        setBindStateChangedListener(this);
    }

    private void postIndeterminateModeHandler() {
        if (indeterminateModeHandler != null) {
            indeterminateModeHandler.postTask(indeterminateModeRunnable, DEFAULT_ANIMATION_DURATION);
        }
    }

    @Override
    public void onComponentBoundToWindow(Component component) {
    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {
        if (progressAnimator != null) {
            progressAnimator.cancel();
        }
        if (indeterminateModeHandler != null) {
            indeterminateModeHandler.removeTask(indeterminateModeRunnable);
        }
    }

    @Override
    public void onRefreshed(Component component) {
        manageColor();
        manageBackgroundProgressBarColor();
        invalidate();
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        canvas.drawOval(new RectFloat(rectF), backgroundPaint);
        float realProgress = (indeterminateMode ? progressIndeterminateMode : progress)
            * DEFAULT_MAX_VALUE / progressMax;

        boolean isToRightFromIndeterminateMode = indeterminateMode && isToRight(progressDirectionIndeterminateMode);
        boolean isToRightFromNormalMode = !indeterminateMode && isToRight(progressDirection);
        float angle = (isToRightFromIndeterminateMode || isToRightFromNormalMode ? 360 : -360) * realProgress / 100;
        Arc arc = new Arc(indeterminateMode ? startAngleIndeterminateMode : startAngle, angle, false);
        canvas.drawArc(new RectFloat(rectF), arc, foregroundPaint);
    }

    @Override
    public void setBackground(Element element) {
        if (element instanceof ShapeElement) {
            RgbColor rgbColor = ((ShapeElement) element).getRgbColors()[0];
            setBackgroundProgressBarColor(new Color(rgbColor.asArgbInt()));
        }
    }

    private void manageColor() {
        Color startColor = progressBarColorStart != null ? progressBarColorStart : progressBarColor;
        Color endColor = progressBarColorEnd != null ? progressBarColorEnd : progressBarColor;
        foregroundPaint.setShader(createLinearGradient(startColor, endColor, progressBarColorDirection),
            Paint.ShaderType.LINEAR_SHADER);
    }

    private void manageBackgroundProgressBarColor() {
        backgroundPaint.setShader(createLinearGradient(
            backgroundProgressBarColorStart != null ? backgroundProgressBarColorStart : backgroundProgressBarColor,
            backgroundProgressBarColorEnd != null ? backgroundProgressBarColorEnd : backgroundProgressBarColor,
            backgroundProgressBarColorDirection), Paint.ShaderType.LINEAR_SHADER);
    }

    private LinearShader createLinearGradient(Color startColor, Color endColor, GradientDirection gradientDirection) {
        float x0 = 0f;
        float y0 = 0f;
        float x1 = 0f;
        float y1 = 0f;
        switch (gradientDirection) {
            case LEFT_TO_RIGHT:
                x1 = (float) getWidth();
                break;
            case RIGHT_TO_LEFT:
                x0 = (float) getWidth();
                break;
            case TOP_TO_BOTTOM:
                y1 = (float) getHeight();
                break;
            case BOTTOM_TO_END:
                y0 = (float) getHeight();
                break;
            default:
                break;
        }
        Point point0 = new Point(x0, y0);
        Point point1 = new Point(x1, y1);
        Point[] points = new Point[]{point0, point1};
        Color[] colors = new Color[]{startColor, endColor};
        return new LinearShader(points, null, colors, Shader.TileMode.CLAMP_TILEMODE);
    }

    // region Measure Method
    @Override
    public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
        int width = Component.EstimateSpec.getSize(widthEstimateConfig);
        int height = Component.EstimateSpec.getSize(heightEstimateConfig);
        int min = Math.min(width, height);
        setEstimatedSize(
            Component.EstimateSpec.getChildSizeWithMode(min, min, Component.EstimateSpec.NOT_EXCEED),
            Component.EstimateSpec.getChildSizeWithMode(min, min, Component.EstimateSpec.NOT_EXCEED));
        float highStroke = Math.max(progressBarWidth, backgroundProgressBarWidth);
        rectF.set((int) (0 + highStroke / 2), (int) (0 + highStroke / 2),
            (int) (min - highStroke / 2), (int) (min - highStroke / 2));
        return true;
    }

    // endregion

    /**
     * Set the progress with animation.
     *
     * @param progress [Float] The progress it should animate to it.
     * @param duration [Long] optional, null by default.
     * @param curveType [CurveType] optional, null by default.
     * @param startDelay [Long] optional, null by default.
     */
    public void setProgressWithAnimation(float progress, Long duration, Integer curveType, Long startDelay) {
        if (progressAnimator != null) {
            progressAnimator.cancel();
        }
        progressAnimator = new AnimatorValue();
        if (duration != null) {
            progressAnimator.setDuration(duration);
        }
        if (curveType != null) {
            progressAnimator.setCurveType(curveType);
        }
        if (startDelay != null) {
            progressAnimator.setDelay(startDelay);
        }
        if (progressAnimator != null) {
            float thisProgress = this.progress;
            float thisProgressIndeterminateMode = this.progressIndeterminateMode;
            progressAnimator.setValueUpdateListener((animatorValue, value) -> {
                float fromFloat;
                if (indeterminateMode) {
                    fromFloat = thisProgressIndeterminateMode;
                } else {
                    fromFloat = thisProgress;
                }

                if (progress >= fromFloat) {
                    float v1 = progress - fromFloat;
                    value = value * v1 + fromFloat;
                } else {
                    float v1 = fromFloat - progress;
                    value = fromFloat - value * v1;
                }

                if (indeterminateMode) {
                    setProgressIndeterminateMode(value);
                } else {
                    setProgress(value);
                }

                if (indeterminateMode) {
                    float updateAngle = value * 360 / 100;
                    setStartAngleIndeterminateMode(DEFAULT_START_ANGLE
                        + (isToRight(progressDirectionIndeterminateMode) ? updateAngle : -updateAngle));
                }
            });
            progressAnimator.start();
        }
    }

    // region Extensions Utils
    private float vpToPx(float vp) {
        return vp * AttrHelper.getDensity(getContext());
    }

    private float pxToVp(float px) {
        return px / AttrHelper.getDensity(getContext());
    }

    private ProgressDirection toProgressDirection(int progressDirection) {
        switch (progressDirection) {
            case 1:
                return ProgressDirection.TO_RIGHT;
            case 2:
                return ProgressDirection.TO_LEFT;
            default:
                throw new IllegalArgumentException("This value is not supported for ProgressDirection: "
                    + progressDirection);
        }
    }

    private ProgressDirection reverse(ProgressDirection progressDirection) {
        return isToRight(progressDirection) ? ProgressDirection.TO_LEFT : ProgressDirection.TO_RIGHT;
    }

    private boolean isToRight(ProgressDirection progressDirection) {
        return progressDirection == ProgressDirection.TO_RIGHT;
    }

    private GradientDirection toGradientDirection(int gradientDirection) {
        switch (gradientDirection) {
            case 1:
                return GradientDirection.LEFT_TO_RIGHT;
            case 2:
                return GradientDirection.RIGHT_TO_LEFT;
            case 3:
                return GradientDirection.TOP_TO_BOTTOM;
            case 4:
                return GradientDirection.BOTTOM_TO_END;
            default:
                throw new IllegalArgumentException("This value is not supported for GradientDirection: "
                    + gradientDirection);
        }
    }

    // endregion

    public void setProgress(float progress) {
        this.progress = this.progress <= progressMax ? progress : progressMax;
        if (onProgressChangeListener != null) {
            onProgressChangeListener.onProgressChange(this.progress);
        }
        invalidate();
    }

    public void setProgressMax(float progressMax) {
        this.progressMax = this.progressMax >= (float) 0 ? progressMax : DEFAULT_MAX_VALUE;
        invalidate();
    }

    public void setProgressBarWidth(float progressBarWidth) {
        this.progressBarWidth = vpToPx(progressBarWidth);
        foregroundPaint.setStrokeWidth(this.progressBarWidth);
        postLayout();
        invalidate();
    }

    public void setBackgroundProgressBarWidth(float backgroundProgressBarWidth) {
        this.backgroundProgressBarWidth = vpToPx(backgroundProgressBarWidth);
        backgroundPaint.setStrokeWidth(this.backgroundProgressBarWidth);
        postLayout();
        invalidate();
    }

    public void setProgressBarColor(Color progressBarColor) {
        this.progressBarColor = progressBarColor;
        manageColor();
        invalidate();
    }

    public void setProgressBarColorStart(Color progressBarColorStart) {
        this.progressBarColorStart = progressBarColorStart;
        manageColor();
        invalidate();
    }

    public void setProgressBarColorEnd(Color progressBarColorEnd) {
        this.progressBarColorEnd = progressBarColorEnd;
        manageColor();
        invalidate();
    }

    public void setProgressBarColorDirection(GradientDirection progressBarColorDirection) {
        this.progressBarColorDirection = progressBarColorDirection;
        manageColor();
        invalidate();
    }

    public void setBackgroundProgressBarColor(Color backgroundProgressBarColor) {
        this.backgroundProgressBarColor = backgroundProgressBarColor;
        manageBackgroundProgressBarColor();
        invalidate();
    }

    public void setBackgroundProgressBarColorStart(Color backgroundProgressBarColorStart) {
        this.backgroundProgressBarColorStart = backgroundProgressBarColorStart;
        manageBackgroundProgressBarColor();
        invalidate();
    }

    public void setBackgroundProgressBarColorEnd(Color backgroundProgressBarColorEnd) {
        this.backgroundProgressBarColorEnd = backgroundProgressBarColorEnd;
        manageBackgroundProgressBarColor();
        invalidate();
    }

    public void setBackgroundProgressBarColorDirection(GradientDirection backgroundProgressBarColorDirection) {
        this.backgroundProgressBarColorDirection = backgroundProgressBarColorDirection;
        manageBackgroundProgressBarColor();
        invalidate();
    }

    public void setRoundBorder(boolean roundBorder) {
        this.roundBorder = roundBorder;
        foregroundPaint.setStrokeCap(roundBorder ? Paint.StrokeCap.ROUND_CAP : Paint.StrokeCap.BUTT_CAP);
        invalidate();
    }

    public void setStartAngle(float startAngle) {
        float angle = startAngle + DEFAULT_START_ANGLE;
        while (angle > 360) {
            angle -= 360;
        }
        this.startAngle = angle < 0 ? 0f : angle > 360 ? 360f : angle;
        invalidate();
    }

    public void setProgressDirection(ProgressDirection progressDirection) {
        this.progressDirection = progressDirection;
        invalidate();
    }

    public void setIndeterminateMode(boolean indeterminateMode) {
        this.indeterminateMode = indeterminateMode;
        if (onIndeterminateModeChangeListener != null) {
            onIndeterminateModeChangeListener.onIndeterminateModeChange(indeterminateMode);
        }
        progressIndeterminateMode = 0f;
        progressDirectionIndeterminateMode = ProgressDirection.TO_RIGHT;
        startAngleIndeterminateMode = DEFAULT_START_ANGLE;
        if (indeterminateModeHandler != null) {
            indeterminateModeHandler.removeTask(indeterminateModeRunnable);
        }
        if (progressAnimator != null) {
            progressAnimator.cancel();
        }
        indeterminateModeHandler = new EventHandler(EventRunner.current());
        if (this.indeterminateMode) {
            if (indeterminateModeHandler != null) {
                indeterminateModeHandler.postTask(indeterminateModeRunnable);
            }
        }
    }

    public void setOnProgressChangeListener(ProgressChangeListener onProgressChangeListener) {
        this.onProgressChangeListener = onProgressChangeListener;
    }

    public void setOnIndeterminateModeChangeListener(IndeterminateModeChangeListener indeterminateModeChangeListener) {
        this.onIndeterminateModeChangeListener = indeterminateModeChangeListener;
    }

    private void setProgressIndeterminateMode(float progressIndeterminateMode) {
        this.progressIndeterminateMode = progressIndeterminateMode;
        invalidate();
    }

    private void setProgressDirectionIndeterminateMode(ProgressDirection progressDirectionIndeterminateMode) {
        this.progressDirectionIndeterminateMode = progressDirectionIndeterminateMode;
        invalidate();
    }

    private void setStartAngleIndeterminateMode(float startAngleIndeterminateMode) {
        this.startAngleIndeterminateMode = startAngleIndeterminateMode;
        invalidate();
    }

    /**
     * ProgressDirection enum class to set the direction of the progress in progressBar
     */
    public enum ProgressDirection {
        TO_RIGHT(1),
        TO_LEFT(2);

        private final int value;

        ProgressDirection(int value) {
            this.value = value;
        }

        public final int getValue() {
            return this.value;
        }
    }

    /**
     * GradientDirection enum class to set the direction of the gradient progressBarColor
     */
    public enum GradientDirection {
        LEFT_TO_RIGHT(1),
        RIGHT_TO_LEFT(2),
        TOP_TO_BOTTOM(3),
        BOTTOM_TO_END(4);

        private int value;

        GradientDirection(int value) {
            this.value = value;
        }

        public int getValue() {
            return this.value;
        }
    }

    /**
     * 进度条监听器
     *
     * @since 2021-07-16
     */
    public interface ProgressChangeListener {
        /**
         * 进度条进度监听回调
         *
         * @param progress 进度
         */
        void onProgressChange(float progress);
    }

    /**
     * 不确定模式监听器
     *
     * @since 2021-07-16
     */
    public interface IndeterminateModeChangeListener {
        /**
         * 不确定模式开关监听回调
         *
         * @param isEnable 开关
         */
        void onIndeterminateModeChange(boolean isEnable);
    }
}
